import { ConfigPreview } from "@/docs/components/Preview";

# External Data Sources

There are several different approaches for loading external data into a Puck component.

It's possible for Puck components to load their own data internally on the client, or on the server using [React server components](/docs/integrating-puck/server-components). This doesn't require any Puck configuration.

If you want to provide the user a way to select the data, you can use the [`external` field type](/docs/api-reference/fields/external).

## Selecting external data

The [`external` field type](/docs/api-reference/fields/external) allows users to select tabular data from a third-party data source, like a headless CMS. This will load the data once and save it into the [data payload](/docs/api-reference/data-model/data).

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        getItemSummary: (item) => item.title,
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
      },
    },
    render: ({ data }) => {
      if (!data) {
        return "No data selected";
      }

      return (
        <>
          <b>{data.title}</b>
          <p>{data.description}</p>
        </>
      );
    },

}}
/>

```tsx {5-17} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            // Query an API for a list of items
            const items = await fetch(`/api/items`).then((res) => res.json());
            // [
            //   { title: "Hello, world", description: "Lorem ipsum 1" },
            //   { title: "Goodbye, world", description: "Lorem ipsum 2" },
            // ];

            return items;
          },
        },
      },
      render: ({ data }) => {
        if (!data) {
          return "No data selected";
        }

        return (
          <>
            <b>{data.title}</b>
            <p>{data.description}</p>
          </>
        );
      },
    },
  },
};
```

You can also use the [`showSearch` parameter](/docs/api-reference/fields/external#showsearch) to show a search input to the user.

## Data syncing

To keep the data in sync with the external source, we can combine the `external` field with the [`resolveData`](/docs/api-reference/configuration/component-config#resolvedatadata-params) function.

This technique re-fetches the content every time the page is loaded, or the [`resolveAllData` utility](/docs/api-reference/functions/resolve-all-data) is called.

```tsx showLineNumbers {19-37} /id: 0/1 /id: 1/ copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            // Query an API for a list of items
            const items = await fetch(`/api/items`).then((res) => res.json());
            // [
            //   { title: "Hello, world", id: 0 },
            //   { title: "Goodbye, world", id: 1 },
            // ];

            return items;
          },
        },
      },
      resolveData: async ({ props }, { changed }) => {
        if (!props.data) return { props };

        // Don't query unless `data` has changed since resolveData was last run
        if (!changed.data) return { props };

        // Re-query the API for a particular item
        const latestData = await fetch(`/api/items/${props.data.id}`).then(
          (res) => res.json()
        );
        // { title: "Hello, world", description: "Lorem ipsum 1", id: 0 }

        return {
          props: {
            // Update the value for `data`
            data: latestData,
          },
        };
      },
      // ...
    },
  },
};
```

## Hybrid authoring

Hybrid authoring enables users to edit fields inline, or populate those fields with data from an external source.

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        getItemSummary: (item) => item.title,
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
      },
      title: {
        type: "text",
      },
    },
    resolveData: async ({ props }) => {
      if (!props.data) return { props,  readOnly: { title: false } };

      return {
        props: { title: props.data.title },
        readOnly: { title: true }
      };
    },
    render: ({ title }) => {
      return (
        <>
          <b>{title}</b>
        </>
      );
    },

}}
/>

This can be achieved by mapping the data from `data.title` to `title` in [`resolveData`](/docs/api-reference/configuration/component-config#resolvedatadata-params), and marking the field as read-only.

```tsx showLineNumbers {21,22} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          // ...
        },
        title: {
          type: "text",
        },
      },
      resolveData: async ({ props }, { changed }) => {
        // Remove read-only from the title field if `data` is empty
        if (!props.data) return { props, readOnly: { title: false } };

        // Don't query unless `data` has changed since resolveData was last run
        if (!changed.data) return { props };

        return {
          props: {
            title: props.data.title,
            readOnly: { title: true },
          },
        };
      },
      render: ({ title }) => <b>{title}</b>,
    },
  },
};
```

## External data packages

We provide helper packages to load data from common data sources.

- [`contentful`](https://github.com/measuredco/puck/tree/main/packages/field-contentful): Select content entries from a [Contentful](https://www.contentful.com) space.

## Further reading

- [`external` field API reference](/docs/api-reference/fields/external)
- [`resolveData` API reference](/docs/api-reference/configuration/component-config#resolvedatadata-params)
- [`resolveAllData` API reference](/docs/api-reference/functions/resolve-all-data)

<div id="puck-portal-root" />
